<?php

/**
 * @copyright Michiel Hakvoort 2010
 * @license http://www.opensource.org/licenses/bsd-license.php New BSD
 * @package mangrove
 * @subpackage core
 * @filesource
 */

/*
 * Copyright (c) 2010 Michiel Hakvoort
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

namespace mg;

use Phar;

interface ClassSource {

    public function getClasses();
    public function getVirtuals();

}

/**
 * A class source for files located within a file system
 *
 * @author Michiel Hakvoort
 *
 */
class FileSystemClassSource implements ClassSource {

    private $path = null;

    public function __construct($path) {
        $canonicalPath = realpath($path);

        if($canonicalPath === false) {
            throw new Exception("Path \"{$path}\" does not exist");
        }

        if(self :: $useCompression === null) {
            self :: $useCompression = extension_loaded('zlib');
        }

        $this->path = $canonicalPath;
    }

    public function getClasses() {

        /* @var $storageManager StorageManager */
        $storageManager = in(function(StorageManager $storageManager) { return $storageManager; });

        /* @var $serializedClasses Storage */
        $serializedClasses = $storageManager->getStorage('filesystemclasssource.cache', false);
        $serializedClasses = $storageManager->cache($serializedClasses);

        /* @var $parsedFiles Storage */
        $parsedFiles = $storageManager->getStorage('filesystemclasssource.parsedfiles', false);
        $parsedFiles = $storageManager->cache($parsedFiles);

        /* @var $fileSystem FileSystem */
        $fileSystem = in(function(FileSystem $fileSystem) { return $fileSystem; });

        $result = array();

        $base64Path = base64_encode($this->path);

        $knownFiles = array();

        if(isset($parsedFiles[$base64Path])) {
            if(self :: $useCompression) {
                $knownFiles = unserialize(gzuncompress($parsedFiles[$base64Path]));
            } else {
                $knownFiles = unserialize($parsedFiles[$base64Path]);
            }
        }

        $newParsedFiles = array();

        if(Phar :: isValidPharFilename($this->path)) {
            $iterator = new \RecursiveIteratorIterator(new Phar($this->path));
        } else {
            $iterator = new \RecursiveIteratorIterator(
            new RegexRecursiveDirectoryFilterIterator(
            new \RecursiveDirectoryIterator($this->path, \RecursiveDirectoryIterator :: CURRENT_AS_FILEINFO)
            ,'#^[^.].*$#i'
            ,'#^[^.].*\.php$#i'));
        }

        foreach($iterator as $filename => $file) {
            if(mb_strtolower(mb_substr($filename, -4)) !== '.php' || !$file->isReadable()) {
                continue;
            }

            $encodedFilename = base64_encode($filename);

            // Remove file from list of known files
            if(isset($knownFiles[$encodedFilename])) {
                unset($knownFiles[$encodedFilename]);
            }

            $fileStatus = $fileSystem->getFileStatus($filename);

            $classes = null;
            	
            if($fileStatus === FileSystem :: FILE_NOT_MODIFIED) {
                if(isset($serializedClasses[$encodedFilename])) {
                    $classes = unserialize($serializedClasses[$encodedFilename]);
                }
            }
            	
            if($classes === null) {
                $content = file_get_contents($filename, FILE_TEXT);

                $tokenizer = new Tokenizer($content);
                	
                $parser = new PhpFileParser($tokenizer, $filename);

                $parsedClasses = $parser->parse();

                $classes = array();

                /* @var $class RawStaticSourceClass */
                foreach($parsedClasses as $class) {
                    	
                    $modifiers = $class->getModifiers();

                    $name = $class->getQualifiedName();

                    $classDefinition = new ClassHeader($name, $modifiers, $class->getFile());
                    	
                    if($class->getParentClass() !== null) {
                        $classDefinition->setParentClass($class->getParentClass());
                    }

                    foreach($class->getImplementedInterfaces() as $interface) {
                        $classDefinition->addInterface($interface);
                    }

                    /* @var $method RawStaticSourceMethod */
                    foreach($class->getMethods() as $method) {
                        $classDefinition->addMethod(new MethodHeader($method->getName(), $method->getModifiers()));
                    }

                    foreach($class->getConstants() as $key => $value) {
                        $classDefinition->setConstant($key, $value);
                    }

                    $classes[] = $classDefinition;
                }

                unset($tokenizer);
                unset($parser);

                if(gc_enabled()) {
                    gc_collect_cycles();
                }

                $serializedClasses[$encodedFilename] = serialize($classes);

            }

            if(!empty($classes)) {
                $result = array_merge($result, $classes);
            }

            $newParsedFiles[$encodedFilename] = true;
        }

        // Clean up classes from removed files (prevent files from ever growing and growing)
        foreach($knownFiles as $knownFile => $value) {
            unset($serializedClasses[$knownFile]);
        }

        if(self :: $useCompression) {
            $parsedFiles[$base64Path] = gzcompress(serialize($newParsedFiles), 9);
        } else {
            $parsedFiles[$base64Path] = serialize($newParsedFiles);
        }

        return $result;
    }

    public function getVirtuals() {
        return array();
    }

    private static $useCompression = null;

}

class VirtualClassSource implements ClassSource {

    protected $virtual;

    public function __construct($virtual) {
        $this->virtual = $virtual;
    }

    public function getClasses() {
        return array();
    }

    public function getVirtuals() {
        return array($this->virtual);
    }

}

/**
 * A ClassSource for internally defined classes
 *
 * @author Michiel Hakvoort
 *
 */
class InternalClassSource implements ClassSource {

    public function __construct() {

    }

    public function getClasses() {
        $result = array();

        $declaredClasses = get_declared_classes();
        $declaredInterfaces = get_declared_interfaces();

        $declaredClasses = array_merge($declaredClasses, $declaredInterfaces);

        foreach($declaredClasses as $declaredClass) {
            $reflectionClass = new \ReflectionClass($declaredClass);

            if(!$reflectionClass->isInternal()) {
                continue;
            }

            $modifiers = 0x00;

            $modifiers |= $reflectionClass->isAbstract() ? PhpModifiers :: IS_ABSTRACT : 0x00;
            $modifiers |= $reflectionClass->isFinal() ? PhpModifiers :: IS_FINAL : 0x00;
            $modifiers |= $reflectionClass->isInterface() ? PhpModifiers :: IS_INTERFACE : 0x00;

            $classDefinition = new ClassHeader($reflectionClass->getName(), $modifiers);
            	
            //			var_dump($classDefinition);

            // Add the parent classes
            $parent = $reflectionClass->getParentClass();

            if($parent !== false) {
                $classDefinition->setParentClass($parent->getName());
            }


            foreach($reflectionClass->getInterfaces() as $interface) {
                // When the parent implements the interface, assume the class inherits it from the parent
                if($parent !== false && $parent->implementsInterface($interface->getName())) {
                    continue;
                }

                $classDefinition->addInterface($interface->getName());
            }

            // Add the methods
            foreach($reflectionClass->getMethods() as $method) {
                if($method->getDeclaringClass() == $reflectionClass) {
                    $classDefinition->addMethod(new MethodHeader($method->getName(), $method->getModifiers()));
                }
            }

            // Add the (scalar) constants
            foreach($reflectionClass->getConstants() as $name => $value) {
                $classDefinition->setConstant($name, $value);
            }
            	
            $result[] = $classDefinition;
        }

        return $result;
    }

    public function getVirtuals() {
        return array();
    }
}

/**
 * A class source for phar files located on the hard drive
 *
 * @author Michiel Hakvoort
 *
 */
class PharFileClassSource extends FileSystemClassSource {

    public function __construct($filename) {
        if(!Phar :: isValidPharFilename($filename)) {
            throw new Exception("\"{$filename}\" is not a valid Phar file");
        }

        parent :: __construct($filename);
    }

    public function getVirtuals() {
        return array();
    }
}